/*
 * SPDX-License-Identifier: Apache-2.0
 */

package org.hyperledger.fabric.samples.fabcar;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import org.hyperledger.fabric.shim.ledger.KeyValue;
import org.hyperledger.fabric.shim.ledger.QueryResultsIterator;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;

public final class FabCarTest {

    private final class MockKeyValue implements KeyValue {

        private final String key;
        private final String value;

        MockKeyValue(final String key, final String value) {
            super();
            this.key = key;
            this.value = value;
        }

        @Override
        public String getKey() {
            return this.key;
        }

        @Override
        public String getStringValue() {
            return this.value;
        }

        @Override
        public byte[] getValue() {
            return this.value.getBytes();
        }

    }

    private final class MockFileModelResultsIterator implements QueryResultsIterator<KeyValue> {

        private final List<KeyValue> carList;

        MockFileModelResultsIterator() {
            super();

            carList = new ArrayList<KeyValue>();

            carList.add(new MockKeyValue("CAR0",
                    "{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}"));
            carList.add(new MockKeyValue("CAR1",
                    "{\"filePath\":\"red\",\"fileMd5\":\"Ford\",\"owner\":\"Brad\"}"));
            carList.add(new MockKeyValue("CAR2",
                    "{\"filePath\":\"green\",\"fileMd5\":\"Hyundai\",\"owner\":\"Jin Soo\"}"));
            carList.add(new MockKeyValue("CAR7",
                    "{\"filePath\":\"violet\",\"fileMd5\":\"Fiat\",\"owner\":\"Pari\"}"));
            carList.add(new MockKeyValue("CAR9",
                    "{\"filePath\":\"brown\",\"fileMd5\":\"Holden\",\"owner\":\"Shotaro\"}"));
        }

        @Override
        public Iterator<KeyValue> iterator() {
            return carList.iterator();
        }

        @Override
        public void close() throws Exception {
            // do nothing
        }

    }

    @Test
    public void invokeUnknownTransaction() {
        FabFileModel contract = new FabFileModel();
        Context ctx = mock(Context.class);

        Throwable thrown = catchThrowable(() -> {
            contract.unknownTransaction(ctx);
        });

        assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
                .hasMessage("Undefined contract method called");
        assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null);

        verifyZeroInteractions(ctx);
    }

    @Nested
    class InvokeQueryFileModelTransaction {

        @Test
        public void whenFileModelExists() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0"))
                    .thenReturn("{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}");

            FileModel car = contract.queryFileModel(ctx, "CAR0");

            assertThat(car).isEqualTo(new FileModel("Toyota", "Prius", "Tomoko"));
        }

        @Test
        public void whenFileModelDoesNotExist() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0")).thenReturn("");

            Throwable thrown = catchThrowable(() -> {
                contract.queryFileModel(ctx, "CAR0");
            });

            assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
                    .hasMessage("FileModel CAR0 does not exist");
            assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_NOT_FOUND".getBytes());
        }
    }

    @Test
    void invokeInitLedgerTransaction() {
        FabFileModel contract = new FabFileModel();
        Context ctx = mock(Context.class);
        ChaincodeStub stub = mock(ChaincodeStub.class);
        when(ctx.getStub()).thenReturn(stub);

        contract.initLedger(ctx);

        InOrder inOrder = inOrder(stub);
        inOrder.verify(stub).putStringState("CAR0",
                "{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}");
        inOrder.verify(stub).putStringState("CAR1",
                "{\"filePath\":\"red\",\"fileMd5\":\"Ford\",\"owner\":\"Brad\"}");
        inOrder.verify(stub).putStringState("CAR2",
                "{\"filePath\":\"green\",\"fileMd5\":\"Hyundai\",\"owner\":\"Jin Soo\"}");
        inOrder.verify(stub).putStringState("CAR3",
                "{\"filePath\":\"yellow\",\"fileMd5\":\"Volkswagen\",\"owner\":\"Max\"}");
        inOrder.verify(stub).putStringState("CAR4",
                "{\"filePath\":\"black\",\"fileMd5\":\"Tesla\",\"owner\":\"Adrian\"}");
        inOrder.verify(stub).putStringState("CAR5",
                "{\"filePath\":\"purple\",\"fileMd5\":\"Peugeot\",\"owner\":\"Michel\"}");
        inOrder.verify(stub).putStringState("CAR6",
                "{\"filePath\":\"white\",\"fileMd5\":\"Chery\",\"owner\":\"Aarav\"}");
        inOrder.verify(stub).putStringState("CAR7",
                "{\"filePath\":\"violet\",\"fileMd5\":\"Fiat\",\"owner\":\"Pari\"}");
        inOrder.verify(stub).putStringState("CAR8",
                "{\"filePath\":\"indigo\",\"fileMd5\":\"Tata\",\"owner\":\"Valeria\"}");
        inOrder.verify(stub).putStringState("CAR9",
                "{\"filePath\":\"brown\",\"fileMd5\":\"Holden\",\"owner\":\"Shotaro\"}");
    }

    @Nested
    class InvokeCreateFileModelTransaction {

        @Test
        public void whenFileModelExists() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0"))
                    .thenReturn("{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}");

            Throwable thrown = catchThrowable(() -> {
                contract.createFileModel(ctx, "CAR0", "Nissan", "Leaf", "Siobhán");
            });

            assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
                    .hasMessage("FileModel CAR0 already exists");
            assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_ALREADY_EXISTS".getBytes());
        }

        @Test
        public void whenFileModelDoesNotExist() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0")).thenReturn("");

            FileModel car = contract.createFileModel(ctx, "CAR0", "Nissan", "Leaf", "Siobhán");

            assertThat(car).isEqualTo(new FileModel("Nissan", "Leaf", "Siobhán"));
        }
    }

    @Test
    void invokeQueryAllFileModelsTransaction() {
        FabFileModel contract = new FabFileModel();
        Context ctx = mock(Context.class);
        ChaincodeStub stub = mock(ChaincodeStub.class);
        when(ctx.getStub()).thenReturn(stub);
        when(stub.getStateByRange("CAR1", "CAR99")).thenReturn(new MockFileModelResultsIterator());

        String cars = contract.queryAllFiles(ctx);

        assertThat(cars).isEqualTo("[{\"key\":\"CAR0\","
                + "\"record\":{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}},"
                + "{\"key\":\"CAR1\","
                + "\"record\":{\"filePath\":\"red\",\"fileMd5\":\"Ford\",\"owner\":\"Brad\"}},"
                + "{\"key\":\"CAR2\","
                + "\"record\":{\"filePath\":\"green\",\"fileMd5\":\"Hyundai\",\"owner\":\"Jin Soo\"}},"
                + "{\"key\":\"CAR7\","
                + "\"record\":{\"filePath\":\"violet\",\"fileMd5\":\"Fiat\",\"owner\":\"Pari\"}},"
                + "{\"key\":\"CAR9\","
                + "\"record\":{\"filePath\":\"brown\",\"fileMd5\":\"Holden\",\"owner\":\"Shotaro\"}}]");
    }

    @Nested
    class ChangeFileModelOwnerTransaction {

        @Test
        public void whenFileModelExists() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0"))
                    .thenReturn("{\"filePath\":\"blue\",\"fileMd5\":\"Toyota\",\"owner\":\"Tomoko\"}");

            FileModel car = contract.changeFileOwner(ctx, "CAR0", "Dr Evil");

            assertThat(car).isEqualTo(new FileModel("Toyota", "Prius", "Dr Evil"));
        }

        @Test
        public void whenFileModelDoesNotExist() {
            FabFileModel contract = new FabFileModel();
            Context ctx = mock(Context.class);
            ChaincodeStub stub = mock(ChaincodeStub.class);
            when(ctx.getStub()).thenReturn(stub);
            when(stub.getStringState("CAR0")).thenReturn("");

            Throwable thrown = catchThrowable(() -> {
                contract.changeFileOwner(ctx, "CAR0", "Dr Evil");
            });

            assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause()
                    .hasMessage("FileModel CAR0 does not exist");
            assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("CAR_NOT_FOUND".getBytes());
        }
    }
}
